﻿using InputHandlers.NativeCode;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

namespace InputHandlers.KeyboardHandling
{
    public static class KeyboardGlobalInterceptor
    {
        private const int WH_KEYBOARD_LL = 13;
        private const int WM_KEYDOWN = 0x0100;
        private const int WM_SYSKEYDOWN = 0x104;
        private const int WM_KEYUP = 0x0101;
        private const int WM_SYSKEYUP = 0x105;

        private static readonly ExtendedKeysExpectationManager s_keysExpectation = new ExtendedKeysExpectationManager(Keys.Alt | Keys.Control | Keys.Shift | Keys.Sleep);

        private static NativeMethods.LowLevelKeyboardProc s_hookProcedure = HookCallback;
        private static IntPtr s_hookID = IntPtr.Zero;
        private static Thread s_messagePump = null;
        private static Keys s_lastPressedKey = Keys.None;
        private static volatile bool s_enable = true;
        private static volatile bool s_inputConsumed = false;

        /// <summary>
        /// Occurs when a key is pressed. Occurs only once, and not another time, till the key is released.
        /// </summary>
        public static event KeyEventHandler KeyPressed = delegate { };

        /// <summary>
        /// Occurs when a key is pressed. Occurs every time there's a 'WM_KEYDOWN' event.
        /// </summary>
        public static event KeyEventHandler KeyDown = delegate { };

        /// <summary>
        /// Occurs when a key is released.
        /// </summary>
        public static event KeyEventHandler KeyReleased = delegate { };

        /// <summary>
        /// Gets or sets a value indicating whether the hooks set for this
        /// <see cref="KeyboardGlobalInterceptor"/> are enabled.
        /// </summary>
        public static bool Enabled
        {
            get
            {
                return s_enable;
            }

            set
            {
                s_enable = value;
            }
        }

        /// <summary>
        /// Set the current (next) system event as consumed, meaning it (and it alone) will not continue to the next of the system hooks' chain.
        /// </summary>
        public static void SetInputConsumed()
        {
            s_inputConsumed = true;
        }

        /// <summary>
        /// Gets the currently pressed extended keys.
        /// </summary>
        /// <value>The currently pressed extended keys.</value>
        public static Keys CurrentlyPressedExtendedKeys
        {
            get
            {
                return s_keysExpectation.ExtendedKeysLastInvoked;
            }
        }

        public static void ApplyKeyboardHook()
        {
            s_hookID = SetHook(s_hookProcedure);
            Application.Run();
            NativeMethods.UnhookWindowsHookEx(s_hookID);
        }

        /// <summary>
        /// Running the message-pump to monitor the keyboard.
        /// </summary>
        public static void ApplyAsyncKeyboardHook()
        {
            s_messagePump = new Thread(ApplyKeyboardHook);

            s_messagePump.IsBackground = false;

            s_messagePump.Start();
        }

        /// <summary>
        /// Disjoins the keyboard hook and ends the message-pump monitoring the keyboard.
        /// </summary>
        public static void DenyAsyncKeyboardHook()
        {
            if (s_messagePump != null)
            {
                s_messagePump.Abort();
                s_messagePump = null;

                NativeMethods.UnhookWindowsHookEx(s_hookID);
            }
        }

        private static IntPtr SetHook(NativeMethods.LowLevelKeyboardProc proc)
        {
            using (Process curProcess = Process.GetCurrentProcess())
            using (ProcessModule curModule = curProcess.MainModule)
            {
                return NativeMethods.SetWindowsHookEx(WH_KEYBOARD_LL, proc, NativeMethods.GetModuleHandle(curModule.ModuleName), 0);
            }
        }

        private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (!Enabled)
            {
                return NativeMethods.CallNextHookEx(s_hookID, nCode, wParam, lParam);
            }

            var vkCode = (Keys)(Marshal.ReadInt32(lParam));

            if (nCode >= 0 &&
                (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN))
            {
                var specialKeys = s_keysExpectation.UpdateAndGetExtendedKeys(vkCode, true);
                var keyArgs = new KeyEventArgs(vkCode | specialKeys);

                KeyDown(null, keyArgs);

                if (vkCode != s_lastPressedKey)
                {
                    KeyPressed(null, keyArgs);
                    s_lastPressedKey = vkCode;
                }
            }

            if (nCode >= 0 &&
                (wParam == (IntPtr)WM_KEYUP || wParam == (IntPtr)WM_SYSKEYUP))
            {
                Keys specialKeys = s_keysExpectation.UpdateAndGetExtendedKeys(vkCode, false);

                KeyReleased(null, new KeyEventArgs(vkCode));

                s_lastPressedKey = Keys.None;
            }

            if (s_inputConsumed)
            {
                s_inputConsumed = false;
                return (IntPtr)1;
            }

            return NativeMethods.CallNextHookEx(s_hookID, nCode, wParam, lParam);
        }
    }
}